4-2 RBAC角色权限功能分析
官方基础 RBAC 方案分析
NestJS 官方文档提供了基础的 RBAC 实现方案,核心逻辑如下:
官方方案架构
1. 定义枚举角色(写死在代码中)
enum Role { User = 'user', Admin = 'admin' }
2. 创建装饰器(打标签)
@Roles(Role.Admin)
3. 创建 Guard(读取标签 + 比对用户角色)
RolesGuard → Reflector 读取装饰器元数据 → 与 request.user.roles 比对
text
// 官方示例:角色枚举
export enum Role {
User = 'user',
Admin = 'admin',
}
// 官方示例:装饰器
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
// 官方示例:Guard
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
typescript
官方方案的局限性
| 局限点 | 说明 |
|---|---|
| 角色写死在枚举中 | 新增角色需要修改代码并重新部署 |
| 无法动态管理 | 不能通过管理界面实时调整角色和权限 |
| 粒度固定 | 只能控制到角色级别,无法细化到具体操作权限 |
| 用户角色硬编码 | 角色与路由的映射关系固定在程序中 |
生产级 RBAC 方案设计
基于官方方案的思路,将角色和权限信息存储到数据库中,实现动态管理。
两层关系映射
┌─────────────────────────────────────────────────┐
│ 数据库层面 │
│ │
│ User ──(多对多)── Role ──(多对多)── Permission │
│ │
│ 用户拥有多个角色,每个角色拥有多个权限 │
└───────────────────────┬─────────────────────────┘
│ 查询
┌───────────────────────┼─────────────────────────┐
│ 程序层面 │
│ ↓ │
│ Module → Controller → Route │
│ │
│ 通过装饰器给 Controller 和 Route 打上唯一标识 │
│ Guard 中读取标识并与数据库权限比对 │
└─────────────────────────────────────────────────┘
text
权限标识设计
为什么不直接使用模块名和路由名?因为开发过程中这些名称可能被修改,一旦修改后数据库中没有同步更新,权限就会失效。
解决方案:为每个 Controller 和 Route 定义独立的唯一字符串标识。
权限标识 = Controller标识 + Route标识
示例:
Controller: @Module('user') → "user"
Route: @Permission('read') → "read"
最终权限标识: "user:read"
text
// 自定义装饰器:模块级标识
import { SetMetadata } from '@nestjs/common';
export const MODULE_KEY = 'module';
export const Module = (name: string) => SetMetadata(MODULE_KEY, name);
// 自定义装饰器:路由级标识
export const PERMISSION_KEY = 'permission';
export const Permission = (name: string) => SetMetadata(PERMISSION_KEY, name);
typescript
Guard 权限匹配流程
请求进入
│
├── 1. Reflector 读取 Controller 上的 @Module 标识
│ → 如 "user"
│
├── 2. Reflector 读取 Route 上的 @Permission 标识
│ → 如 "read"
│
├── 3. 拼接权限标识
│ → "user:read"
│
├── 4. 从 JWT 获取用户 → 查询用户角色 → 查询角色权限
│ → ["user:read", "user:create", "user:update"]
│
└── 5. 判断权限列表是否包含 "user:read"
→ true: 放行 | false: 403 Forbidden
text
对比:官方方案 vs 生产级方案
| 对比维度 | 官方方案 | 生产级方案 |
|---|---|---|
| 角色存储 | 枚举(硬编码) | 数据库表 |
| 权限粒度 | 角色级别 | 模块 + 路由级别 |
| 权限标识 | 角色名称 | 唯一字符串组合 |
| 动态管理 | 不支持 | 支持运行时调整 |
| 适用场景 | 简单应用 | 企业级应用 |
| 实现复杂度 | 低 | 中 |
实现步骤规划
- 修改数据库模型:添加 Role、Permission 及其关联表
- 创建装饰器:定义模块级和路由级的唯一标识装饰器
- 创建 Guard:读取装饰器元数据并查询数据库进行权限比对
- 实现 CRUD:角色和权限的管理接口
- 关联用户:用户创建和更新时关联角色
预习建议
理解上述方案后,建议参考 NestJS 官方的基础 RBAC 代码,尝试从数据库设计开始,自行实现一套基于数据库的 RBAC 权限控制系统。
↑